package jmine.tec.test.utils.thread;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.apache.log4j.Logger;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.impl.SessionFactoryImpl;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;

/**
 * Factory bean that returns a session factory that is bound to the current thread.
 * 
 * @author jrenaut
 */
public class ThreadBoundSessionFactoryFactoryBean extends AnnotationSessionFactoryBean {

    private static final Logger LOGGER = Logger.getLogger(ThreadBoundSessionFactoryFactoryBean.class);

    // private SessionFactory sessionFactory;

    /**
     * Searches for the URL property in the configuration. If found attempts to replace the database identifier token with the hashcode from
     * the current thread.
     * 
     * @param config {@link AnnotationConfiguration}
     */
    @Override
    protected void postProcessAnnotationConfiguration(final AnnotationConfiguration config) {
        super.postProcessAnnotationConfiguration(config);

        String url = config.getProperty(Environment.URL);
        if (url.indexOf("#id#") > -1) {
            url = url.replaceAll("#id#", Integer.toHexString(Thread.currentThread().hashCode()));
            config.setProperty(Environment.URL, url);
        }
    }

    /**
     * @return SessionFactory
     * @throws Exception ex
     */
    @Override
    protected SessionFactory buildSessionFactory() throws Exception {
        return (SessionFactory) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{
                SessionFactory.class, SessionFactoryImplementor.class },
                new ThreadBoundSessionFactoryInvocationHandler());
    }

    /**
     * @author jrenaut
     */
    private class ThreadBoundSessionFactoryInvocationHandler implements InvocationHandler {

        private final ThreadLocal<SessionFactory> factories = new ThreadLocal<SessionFactory>();

        /**
         * Searches if a Session Factory has already been issued for this thread. If it hasn't delegates creation to the factgory bean and
         * stores the created factory in a thread local. If it has returns the factory already placed in the thread local.
         * 
         * @return {@link SessionFactoryImpl}
         */
        private SessionFactoryImpl getDelegate() {
            SessionFactory sf = this.factories.get();
            if (sf == null) {
                try {
                    sf = ThreadBoundSessionFactoryFactoryBean.super.buildSessionFactory();
                    this.factories.set(sf);
                } catch (Exception e) {
                    throw new AssertionError("Could not get a Session Factory!");
                }
            }
            LOGGER.info(String.format("Thread [%d] is being issued session factory [%d]", Thread.currentThread()
                    .hashCode(), sf.hashCode()));
            return (SessionFactoryImpl) sf;
        }

        /**
         * @param proxy Object
         * @param method Method
         * @param args Object[]
         * @return object
         * @throws Throwable t
         */
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            try {
                return method.invoke(this.getDelegate(), args);
            } catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
        }

    }
}
